define([], function () {

    var $tiles = {};

    $tiles.lngLatToTile = function (lng, lat, zoom) {
        var n = Math.pow(2, zoom);
        var xtile = ((lng + 180.) / 360.0) * n;
        var lat_rad = lat / 180.0 * Math.PI;
        var ytile = (1 - (Math.log(Math.tan(lat_rad) + 1 / Math.cos(lat_rad)) / Math.PI)) / 2 * n;
        return [~~xtile, ~~ytile];
    };

    $tiles.tile2lngLat = function (tileX, tileY, pixelX, pixelY, tileSize, zoom) {
        var lng = (tileX + (pixelX * 1.0 / tileSize)) * 360.0 / (Math.pow(2, zoom)) - 180.0;
        var lat = Math.atan(Math.sinh(Math.PI - 2 * Math.PI * (tileY + pixelY / tileSize) / (Math.pow(2, zoom)))) * 180.0 / Math.PI;
        return [lng, lat];
    };

    $tiles.collectTilesRect = function (lng, lat, meters, zoom) {

        var tiles = [];

        $tiles.queryTileRange(lng, lat, meters, zoom, function (minX, maxX, minY, maxY) {
            if (maxX >= minX && maxY >= minY) {
                for (var i = minX; i <= maxX; i++) {
                    for (var j = minY; j <= maxY; j++) {
                        tiles.push([i, j, zoom]);
                    }
                }
            }
        });

        return tiles;
    };

    $tiles.queryTileRange = function (lng, lat, meters, zoom, onRange) {
        var dlng = $tiles.meterToLngAtLat(lat, meters);
        var dlat = $tiles.meterToLat(meters);

        var coord1 = $tiles.lngLatToTile(lng + dlng, lat + dlat, zoom);
        var coord2 = $tiles.lngLatToTile(lng + dlng, lat - dlat, zoom);
        var coord3 = $tiles.lngLatToTile(lng - dlng, lat + dlat, zoom);
        var coord4 = $tiles.lngLatToTile(lng - dlng, lat - dlat, zoom);

        var minX = ~~Math.min(Math.min(coord1[0], coord2[0]), Math.min(coord3[0], coord4[0]));
        var maxX = ~~Math.max(Math.max(coord1[0], coord2[0]), Math.max(coord3[0], coord4[0]));
        var minY = ~~Math.min(Math.min(coord1[1], coord2[1]), Math.min(coord3[1], coord4[1]));
        var maxY = ~~Math.max(Math.max(coord1[1], coord2[1]), Math.max(coord3[1], coord4[1]));

        onRange && onRange(minX, maxX, minY, maxY);
    };

    $tiles.collectTilesRectBetween = function (lng, lat, meters, zooms) {
        var tiles = [];
        (zooms || []).forEach(function (z) {
            tiles = tiles.concat($tiles.collectTilesRect(lng, lat, meters, z));
        });
        return tiles;
    };

    /*
     * For short distances on WGS84 coords
     */
    $tiles.lngToMeterAtLat = function (lat, delta) {
//    return delta * Math.cos(Math.PI * Math.abs(lat) / 180) * 40075040 / 360.0;
        //from https://www.wikiwand.com/en/Geographic_coordinate_system#/Expressing_latitude_and_longitude_as_linear_units
        var perDegree = $tiles.meterPerDegreeLngAtLat(lat);

        return perDegree * delta;
    };

    $tiles.latToMeter = function (lat, delta) {
        //from javascript
//    return 40008000 * delta / 360.0;
        //from https://www.wikiwand.com/en/Geographic_coordinate_system#/Expressing_latitude_and_longitude_as_linear_units
        var perDegree = $tiles.meterPerDegreeAtLat(lat);

        return perDegree * delta;
    };

    $tiles.meterToLat = function (delta) {
        return 360.0 * delta / 40008000;
    };

    $tiles.meterToLngAtLat = function (lat, delta) {
        return 360.0 * delta / (40075040 * Math.cos(Math.PI * Math.abs(lat) / 180.0));
    };

    $tiles.meterPerDegreeLngAtLat = function (lat) {
        lat = (lat * Math.PI) / 180.0;
        return 111412.84 * Math.cos(lat) - 93.5 * Math.cos(lat * 3) + 0.118 * Math.cos(lat * 5);
    };

    $tiles.meterPerDegreeAtLat = function (lat) {
        //lat = Mathf.Deg2Rad(lat);
        lat = (lat * Math.PI) / 180.0;
        return 111132.92 - 559.82 * Math.cos(lat * 2) + 1.175 * Math.cos(lat * 4) - 0.0023 * Math.cos(lat * 6);
    };


    Math.radians = function (degrees) {
        return degrees * Math.PI / 180;
    };
    Math.degrees = function (radians) {
        return radians * 180 / Math.PI;
    };
    Math.sinh = function (x) {
        return (Math.exp(x) - Math.exp(-x)) / 2;
    };

    var createCenterLngLat = function (lng, lat) {
        var meterForLng = 6378137 * Math.cos(Math.radians(lat)) * 2 * Math.PI / 360;
        var meterForLat = 6378137 * 2 * Math.PI / 360;
        return {lng: lng, lat: lat, meterForLng: meterForLng, meterForLat: meterForLat}
    };

    $tiles.lnglatMove = function (lnglat, eastMeters, northMeters) {
        var cll = createCenterLngLat(lnglat.lng, lnglat.lat);
        cll.lng += eastMeters / cll.meterForLng;
        cll.lat -= northMeters / cll.meterForLat;
        return cll;
    };

    $tiles.lnglatDiff = function (lnglat, clnglat) {
        if (clnglat.meterForLng === undefined || clnglat.meterForLat === undefined) {
            var cll = createCenterLngLat(clnglat.lng, clnglat.lat);
            clnglat.meterForLng = cll.meterForLng;
            clnglat.meterForLat = cll.meterForLat;
        }
        var dlng = lnglat.lng - clnglat.lng;
        var dlat = lnglat.lat - clnglat.lat;
        var dx = (dlng * clnglat.meterForLng); //x -> east
        var dz = (-dlat * clnglat.meterForLat); //z -> south
        return {dx: dx, dz: dz};
    };

    $tiles.tileToLngLat = function (tileX, tileY, zoom) {
        zoom = zoom || 15;
        var n = 1 << zoom;
        var lng = tileX / n * 360 - 180;
        var lat = Math.degrees(Math.atan(Math.sinh(Math.PI - tileY / n * Math.PI * 2)));
        return {lng: lng, lat: lat};
    };

    $tiles.lnglatToTile = function (lng, lat, zoom) {
        zoom = zoom || 15;
        var x = Math.floor((lng + 180) / 360 * (1 << zoom));
        var y = Math.floor((1.0 - Math.log(Math.tan(Math.radians(lat)) + 1.0 / Math.cos(Math.radians(lat))) / Math.PI) / 2.0 * (1 << zoom));
        return {tileX: x, tileY: y};
    };

    return $tiles;
});